Skip to content

fix(host-chat): drop private flag so the package is publishable#180

Merged
johnthecat merged 17 commits into
release/0.8from
fix/publish-host-chat
May 22, 2026
Merged

fix(host-chat): drop private flag so the package is publishable#180
johnthecat merged 17 commits into
release/0.8from
fix/publish-host-chat

Conversation

@kalininilya

Copy link
Copy Markdown
Contributor

Summary

  • Remove "private": "true" from packages/host-chat/package.json so the release pipeline ships it alongside the rest of the V2 SDK surface.

Background

The 0.8.0-0 release published host-api, host-papp, scale, etc. but skipped host-chat because of the private flag. Downstream desktop pins it to file:../triangle-js-sdks/packages/host-chat, which only works for local dev and breaks CI consumers that don't have the sibling SDK checkout.

Test plan

  • Confirm release pipeline picks up host-chat on next publish
  • After publish, drop the file: ref in polkadot-desktop/package.json and reinstall

…ng service

Wire-compatible with the V2 SSO protocol:
  VersionedHandshakeProposal::V2 — emitted by the host via QR carrying
    Device { statementAccountId, encryptionPublicKey } and metadata
    (HostName / HostVersion / HostIcon / PlatformType / PlatformVersion / Custom)
  VersionedHandshakeResponse — answer over Statement Store, ECDH-encrypted
    body; inner payload is `EncryptedHandshakeResponseV2 = Pending | Success
    | Failed`. Success carries identity_chat_pubkey + identity_sr25519_pubkey
    + 64-byte sr25519 identity_signature.

Pieces:
  - scale/handshakeV2.ts — SCALE codecs. V2 is at discriminant 1 (with a
    `_v1Reserved: _void` slot to push it past the legacy V1 index 0).
    EncryptedHandshakeResponseV2 is a length-dispatched custom codec
    (1 byte / 161 bytes / variable str) since the peer's SCALE library
    elides the outer enum index for class-wrapped sealed-interface variants.
  - v2/topic.ts — pairing topic + channel derivation:
      khash(statementAccountId, encryptionPublicKey || "topic"|"channel")
  - v2/proposal.ts — encode + build `polkadotapp://pair?handshake=<hex>`.
  - v2/envelope.ts — ECDH (P-256) + AES-GCM via @novasamatech/statement-store
    createEncryption.
  - v2/state.ts — Idle -> Submitted -> Pending(AllowanceAllocation) ->
    Success | Failed; forward-only transitions with same-tag idempotence.
  - v2/service.ts — orchestrator: subscribes + polls the pairing topic,
    SCALE-decodes statements, drives the state machine. Exposes optional
    initialProcessedDataHex + onStatementProcessed so callers can
    persist byte-level dedupe across reloads (e.g. for proper logout).

Adds rxjs and @polkadot-api/utils to host-papp deps. 60 new tests.
Adds a `## V2 SSO handshake` section covering:
  - protocol overview vs V1 (incompat)
  - flow diagram (host ↔ peer)
  - building the proposal QR via `buildPairingDeeplink`
  - driving the handshake via `startPairingV2`, the state machine, and abort
  - surviving reloads + proper logout via `initialProcessedDataHex` /
    `onStatementProcessed` byte-level dedupe
  - topic/channel derivation helpers
  - SCALE codec exports table (proposal, response, success, signature payload)

The existing "Authentication and pairing" section is now flagged as the V1
flow and links to the V2 section, so callers can pick the right path.
Per the multi-device chat spec (HackMD Ski9naYdWe), PApp shares the user
identity chat keypair with each paired device so the device can decrypt
incoming chat traffic addressed to the user identity. Today's
HandshakeSuccessV2 wire payload only carries the public half; this commit
extends it with a 32-byte raw P-256 private scalar (identityChatPrivateKey)
appended after identitySignature.

Wire impact: Success payload grows from 161 to 193 bytes. The length-
dispatched EncryptedHandshakeResponseV2 codec recognises the new length
as Success; older Pending (1 byte) and Failed (variable string) variants
keep the same shape. Encryption is unchanged - the outer envelope's
ECDH-AES wrap already protects the payload in transit.

Mirrored on the responder side (PApp) per the same spec; consumers
(paired devices) persist the priv only in OS-keychain-backed secure
storage and never forward it.
Initial pass forced HandshakeSuccessV2 to 193 bytes (with the new chat
priv field), which broke pairing against PApp builds that haven't yet
shipped the multi-device extension - the codec saw 161-byte legacy
Success payloads and fell through to the variable-length Failed branch.

Now length-dispatched: 161 bytes decodes as legacy Success with
identityChatPrivateKey = undefined, 193 bytes decodes as the new
extended Success. Encode emits the 193-byte form when a priv key is
provided, otherwise falls back to the legacy struct.

HandshakeSuccessState.identityChatPrivateKey becomes Uint8Array | undefined
so consumers can branch on availability. Send-only V2 paths continue to
work without it; inbound chat-request decryption is gated on the field
being present.

Removes the wire-incompatibility introduced in the previous commit
without rolling back the spec-aligned shape; once every PApp build
ships the priv key the legacy branch can be retired.
…vate_key

The V2 multi-device handshake spec now ships only the user identity
chat P-256 private scalar (32 bytes) on the wire; the matching public
key is derived locally via P-256 scalar multiplication. Wire format
shrinks from 193 to 128 bytes (accountId || identityChatPrivateKey ||
identitySignature). Legacy 161-byte payloads (encryptionKey || accountId
|| signature) are still accepted for PApp builds without the multi-device
extension. identitySignature now commits to (accountId ||
derive_pub(identityChatPrivateKey)).
Align the V2 SSO handshake codec with the multi-device spec
(https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/Ski9naYdWe).

Success body shape:
  identityAccountId(32) || rootAccountId(32) || identityChatPrivateKey(32) ||
  deviceEncPubKey(65) = 161 bytes (spec v0.2.1)

Also accept the 129-byte v0.2 variant (no rootAccountId) emitted by
Android's `feature/location-for-handshake`. Surface `rootAccountId: null`
in that case — chat does not need it; product-account derivation
degrades gracefully.

Removed:
  - `identitySignature` field (multi-device authorisation moves to the
    user-identity-signed roster events DeviceAdded/DeviceRemoved)
  - `IDENTITY_SIGNATURE_PAYLOAD_BYTES` export
  - `HandshakeSuccessV2WithChatPriv` (was the experimental 128-byte shape)

Added:
  - `HandshakeSuccessV2Value` exported type
  - `decodeEncryptedHandshakeResponseV2` — explicit length-dispatched
    decoder for the inner plaintext
  - `deriveIdentityChatPublicKey` — P-256 scalar mult helper
  - `EncryptedHandshakeResponseV2` now built with native scale-ts `Enum`
    on the inner discriminant. The peer SCALE library does NOT elide the
    variant index, so `Pending(AllowanceAllocation)` arrives as `0x00 0x00`
    and was being misclassified as `Failed("")` by the prior length-only
    dispatch.

`HandshakeSuccessState` now exposes identityAccountId, rootAccountId,
identityChatPrivateKey, identityChatPublicKey (derived locally) and
deviceEncPubKey. Pairing service decodes via the new
`decodeEncryptedHandshakeResponseV2` and logs failure reasons with the
raw inner bytes for diagnosis.
V2 multi-device runtime metadata exposes Resources.Consumers fields in
camelCase, which crashed `raw.stmt_store_slots.map(...)` in the host-papp
identity adapter. Read each field with snake/camel fallback and treat
slots as optional. Same defensiveness applied to host-chat's
getConsumerInfo. The .papi descriptor types only model snake_case, so
widen via `Record<string, unknown>` at the read site.
Adopt the multi-device chat content shape per the chat spec v0.1
(https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/Ski9naYdWe).

New MessageContent variants:
  - chatAccepted (14) — legacy single-device accept payload changed from
    `_void` to `ChatAcceptedContent { messageId: String }` for iOS V1
    backward decode.
  - _reserved16 (16) — reserved (Android `coinagePayment`, unused on desktop)
  - deviceAdded (17) — `{ statementAccountId, encryptionPublicKey }`
  - deviceRemoved (18) — `{ statementAccountId }`
  - _reserved19 (19) — placeholder so deviceChatAccepted lands at the
    spec'd index 20.
  - deviceChatAccepted (20) — `{ requestId, device: DeviceInfo }`. Sent
    via identity-level session SessionId(B, A) encrypted with K(A,B);
    identity-level encryption lets all of A's devices decrypt without a
    per-device envelope.

DeviceAdded/Removed use length-prefixed `Bytes()` instead of fixed-size
codecs because substrate-sdk-android emits the `AccountId` /
`EncodedPublicKey` wrapper types without `@FixedLength`, falling through
to length-prefixed `Vec<u8>` on the wire. DeviceInfoContent (used by
deviceChatAccepted) stays fixed-size — Android's `DeviceInfoScale`
declares `@FixedLength` explicitly.
`npm ci` in CI requires package-lock.json to be in sync with
package.json. The V2 SSO commits added @polkadot-api/utils, rxjs, and
verifiablejs to host-papp but the lockfile wasn't refreshed when the
branch was first pushed.
`createAuth` / `pappAdapter.sso` is now V2-driven end-to-end with the same
`pairingStatus` / `authenticate()` / `abortAuthentication()` surface as
before — the V2 wire format, ECDH envelope, pairing service, state
machine, topic derivation, and peer-signer capture all run inside the
SDK. The V1 handshake codec and ephemeral-keypair logic are gone;
callers must inject a persistent `DeviceIdentityForPairing` via a thunk
on `createPappAdapter`.

`createPappAdapter` reshape:
  - drops `metadata: string` (the V1 metadata URL) — host name / icon /
    platform now ride inside `hostMetadata` (which mirrors V2's
    `HandshakeMetadata`)
  - requires `deviceIdentity: () => Promise<DeviceIdentityForPairing>`
  - new `onAuthSuccess`, `initialProcessedDataHex`,
    `onPairingStatementProcessed` callbacks for consumer-side
    persistence and reload-survival dedupe

`AuthSuccess.peerStatementAccountId` is lifted off
`statement.proof.value.signer` during pairing so device-sync can seed
PApp without a follow-up chain query. Threaded through
`HandshakeSuccessState` and `fromInnerResponse`.

Public surface trimmed to `createAuth` and the types you need to use
it. `startPairingV2`, the state-machine helpers (`idle` / `submitted` /
`advance` / `fromInnerResponse` / `isTerminal` /
`canSubmitV2Statements`), topic / envelope / proposal helpers
(`computePairingTopic` / `computePairingChannel` /
`buildPairingDeeplink` / `encodeProposal` / `decryptResponseEnvelope` /
`deriveIdentityChatPublicKey`), every `Handshake*` /
`*HandshakeResponse*` / `VersionedHandshake*` / `MetadataEntry` /
`MetadataKey` / `Device` SCALE codec, and the `Handshake*State` /
`HandshakeMetadata` / `HandshakeProposalDevice` /
`HandshakeResponseEnvelope` / `Pairing` / `StartPairingDeps` types are
all internal now. `PairingStatus.finished` no longer carries `session`
— read the resolved `AuthSuccess` off `authenticate()` instead.

`host-papp-react-ui`:
  - `useAuthStatus` returns `{ status, isSignedIn }` (was `signedInUser`,
    which depended on the V1 session shape)
  - `Flow.stories.tsx` updated for the new `createPappAdapter` params

523/523 tests pass. `auth.spec.ts` rewritten to drive the V2-backed
`createAuth` end-to-end (success, persistOnSuccess hook, Failed inner
response, abort, debug emits) using a real SCALE-encoded
HandshakeSuccessV2 envelope.
The previous draft mixed two reference frames: some items described the
diff vs. 0.7.8 (correct for a changelog), others described the diff vs.
an earlier state of this branch where V2 helpers were briefly public
(internal to PR #178 development, irrelevant to consumers). The latter
read as "no longer something callers need to wire up" / "public surface
trimmed" / "fix: EncryptedHandshakeResponseV2 misclassification" — none
of those make sense for a consumer upgrading from 0.7.8, where V2 SSO
didn't exist publicly at all.

Reframed:
  - Lead Features bullet now states V2 SSO as a fresh capability behind
    the existing createAuth surface, with the V2 codecs/service/state-
    machine flagged as SDK internals (not as "no longer public").
  - Dropped the "public surface trimmed" Breaking Change — those names
    were never in 0.7.8.
  - Dropped the EncryptedHandshakeResponseV2 fix bullet — that was a bug
    introduced and fixed inside this PR, not visible to 0.7.8 users.
  - Merged the legacy-payload-removal bullet into the main V1-removed
    bullet to avoid double-flagging the same break.
…K; restore V1-shape auth surface

The 0.7.x → 0.8.0 migration is now two field renames (`metadata` →
inline `hostMetadata`, `osType`/`osVersion` → `platformType`/
`platformVersion`). Everything else about the auth surface — the
`pairingStatus` / `authenticate()` / `abortAuthentication()` triad,
`PairingStatus.finished.session`, `authenticate()` resolving to
`StoredUserSession | null` — is what it was.

What moved into the SDK:

  - New internal `deviceIdentityStore` (encrypted via the same
    gcm/blake2b pattern as `UserSecretRepository`). `createPappAdapter`
    now defaults `deviceIdentity` to a `StorageAdapter`-backed factory
    that generates and persists a fresh identity on first run. Override
    with the new optional `deviceIdentity` param if you need a
    different backend (Electron Keychain, native secure storage).

  - Pairing-topic statement dedupe (`initialProcessedDataHex` /
    `onPairingStatementProcessed` in the previous draft) is now
    fully internal; the consumer-facing surface drops both.

  - On Success, `createAuth` builds a V2-shaped `StoredUserSession` and
    writes it + the per-session secrets to `ssoSessionRepository` and
    `userSecretRepository` itself. `authenticate()` returns the
    persisted `StoredUserSession`. The optional `onAuthSuccess` hook
    fires after persistence with `{ session, identityChatPrivateKey }`
    so consumers can fan the user-identity bits out to their own
    stores (e.g. polkadot-desktop's `deviceIdentityRepository`).

Schema changes:

  - `StoredUserSession` gains `identityAccountId?` and
    `identityChatPublicKey?` as trailing `Option` fields; the peer
    device statement account is exposed via `remoteAccount.accountId`.
    `from` decoder wraps in try/catch and returns `[]` on V1-blob
    decode failure, so the SDK silently wipes 0.7.x SsoSessions on
    first read instead of crashing.

  - `UserSecretRepository` gains `identityChatPrivateKey: Bytes(32)`
    as a trailing required field. `decode` wraps in try/catch and
    returns `null` on V1-blob decode failure.

  - `DeviceIdentityForPairing` adds a required `statementAccountSecret`
    (64-byte expanded sr25519 secret) so the V1 sessionManager prover
    can sign session statements with the device's stable identity.

Public surface stays compact: `createPappAdapter`, `PappAdapter`,
`AuthComponent`, `HostMetadata`, `OnAuthSuccess`, `PairingStatus`,
`DeviceIdentityForPairing`, `StoredUserSession`, `UserSession`,
`Identity`, signing request/response types, `RingVrf*`,
`SS_*_ENDPOINTS`. `AuthSuccess` is gone — `StoredUserSession` carries
the same fields.

`host-papp-react-ui`:
  - `useAuthStatus` restores `signedInUser` (reads
    `pairingStatus.finished.session`)
  - `Flow.stories.tsx` drops the `deviceIdentity` stub — the SDK now
    defaults it.

524/524 tests pass. New tests cover internal persistence, the
`onAuthSuccess` hook, and the default-deviceIdentity fallback.
V2 People chain endpoints — needed by desktop env constants that have been
falling back to paseo-next while the V2 entry was missing.
host-chat was marked private:"true" and silently skipped by the release
pipeline — the 0.8.0-0 release only shipped host-api/host-papp/etc.
Downstream desktop has to pin host-chat to a file: workspace ref, which
breaks CI consumers that don't have the sibling SDK checkout.

Removing the flag lets the next release publish it alongside the rest of
the V2 SDK surface.
@kalininilya
kalininilya changed the base branch from main to feat/multi-device-sdk-v2-rebased May 22, 2026 13:13
The existing README was an unedited copy of host-container's docs (wrong
title, none of the host-chat surface mentioned). Rewrite to cover what
the package actually does: createAccountService, network selection,
search / getConsumerInfo semantics, and the codec subpath exports.
@johnthecat
johnthecat changed the base branch from feat/multi-device-sdk-v2-rebased to release/0.8 May 22, 2026 13:28
# Conflicts:
#	CHANGELOG.md
#	package-lock.json
#	packages/host-chat/package.json
#	packages/host-papp/package.json
#	packages/host-papp/src/sso/auth/v2/proposal.ts
@johnthecat
johnthecat merged commit ea43c35 into release/0.8 May 22, 2026
5 checks passed
@johnthecat
johnthecat deleted the fix/publish-host-chat branch June 11, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants